Posts by code blue(6)

Page 2 of 2
Bad coffee

React Router 7

How to use the Framework mode

React Router is a multi-strategy router for React, which means it can be used in different ways depending on the needs of your application. In this guide, we’ll explore the framework mode, which is a new way to use React Router that allows you to define your routes in a more declarative way.

Generating a new React project

The framework mode is the one that offers the most features (check comparison table), hence it has a few dependencies than the other modes. For that reason, setting it up manually it’s a bit tricky. A better idea is to generate a new project using the following command:

Terminal window
npx create-react-router@latest my-react-router-app

Where my-react-router-app is the name of our project. This command will generate a new React project with the all the necessary dependencies to use React Router in framework mode.

NOTE

Later in this guide, and as an exercise, we’ll see how to add the framework mode to an already existing React project.

Once we have our project created, we can navigate to the root folder and start the development server:

Terminal window
cd my-react-router-app
npm run dev

Important Files

If we open the project in our code editor, we should see a bunch of files and folders, but the most important ones are inside the app folder:

  • app/root.tsx: This is the entry point of our application, from where we export:
    • The Route.LinksFunction.
    • The Layout component.
    • The App component.
    • An ErrorBoundary component.
  • app/routes.ts: This is where we define our routes.

Defining Routes

Routes must be defined in the app/routes.ts file. For example, let’s define a simple route that renders a route module named home.tsx when the URL is /:

app/routes.ts
import { type RouteConfig, index } from '@react-router/dev/routes'
export default [
route('/', 'routes/home.tsx')
] satisfies RouteConfig

The route function is known as a route matcher; this route matcher takes two arguments:

  • A URL pattern to match the URL, in this case ’/’.
  • A file path to the route module that will be rendered when the URL matches the pattern.

Since having a route for the home page is a common use case, React Router provides a helper function called index that does the same as route('/', 'routes/home.tsx'):

app/routes.ts
import { type RouteConfig, index } from '@react-router/dev/routes'
export default [
route('/', 'routes/home.tsx')
index('routes/home.tsx')
] satisfies RouteConfig

These are the contents of routes/home.tsx:

routes/home.tsx
export default function Index() {
return <h1>You are at Index</h1>
}

IMPORTANT

Note that routes/home.tsx is a route module and not just a React component. We have to export the React component as default, otherwise we’ll get error:

You made a GET request to / but did not provide a `loader` for route "routes/home", so there is no way to handle the request.

Yeah, the error is a bit misleading, because we’re not loading any data in our componet; again, it happens because we didn’t export the component as default.

Adding a Loader

Let’s define data loading for our route module. There are two ways to do this:

Client Data Loading

Let’s add a clientLoader function to fetch a list of TODOs from the JSONPlaceholder API. We’ll render the list of TODOs in the home.tsx route module:

routes/home.tsx
import type { Route } from '../+types/root'
interface Todo {
userId: number
id: number
title: string
completed: boolean
} // Better move this type to a separate file (types/todo.ts) and import it here.
export async function clientLoader() {
const response = await fetch('https://jsonplaceholder.typicode.com/todos')
if (!response.ok) {
throw new Error('Failed to fetch todos')
}
return response.json()
}
export default function Home({ loaderData }: Route.ComponentProps) {
return (
<main>
<h1>You are at Index</h1>
<h2>Todos</h2>
<ul>
{(loaderData ?? []).map((todo: Todo) => (
<li key={todo.id} className="py-4">
<h3>{todo.title}</h3>
<p>{todo.completed ? 'Completed' : 'Not Completed'}</p>
</li>
))}
</ul>
</main>
)
}

Yeah, I know, it’s a lot of code for rendering just a TODO list!, but let’s break it down:

  • We define a Todo type that represents the structure of the data we’re going to fetch.
  • Note that clientLoader runs on the client side, like a traditional SPA; open the network tab to verify the request.
  • In the React component, we destructure the loaderData prop to access the data fetched by the clientLoader function. Then we render the list of TODOs.

TIP

When testing this code, the browser’s console will show a recommendation to use the HydrateFallback, so I added:

// HydrateFallback is rendered while the client loader is running
export function HydrateFallback() {
return <div>Loading...</div>;
}

Nice! So no need of defining React loading states.

Server Data Loading

Server Data Loading works the same way as the clientLoader, but it runs on the server side. Let’s add a loader function to fetch the individual TODOs:

routes/todo.tsx
import type { Route } from '../+types/root'
import { Link } from 'react-router'
import type { Todo } from 'types/todo'
export async function loader({ params }: { params: { id: string } }) {
const { id } = params
const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`)
if (!response.ok) {
throw new Error('Failed to fetch todos!')
}
return await response.json()
}
export default function Todo({ loaderData }: Route.ComponentProps) {
const todo = loaderData ? (loaderData as Todo) : null
return (
<main>
<Link to="/">Back Home</Link>
{todo && (
<div key={todo.id}>
<h3>{todo.title}</h3>
<p>{todo.completed ? 'Completed' : 'Not Completed'}</p>
</div>
)}
</main>
)
}

TIP

Feel free to wrap the TODOs in routes/home.tsx in a Link component to navigate to the individual TODO route with a click.

The loader function runs on the server, so you won’t be able to see the network request in the browser’s console. We get good old HTML from the backend, which you can verify it by disabling JavaScript in the browser.

IMPORTANT

Remember to add a route in app/routes.ts to render the todo.tsx route module:

app/routes.ts
import { type RouteConfig, index, route } from '@react-router/dev/routes'
export default [
index('routes/home.tsx'),
route('todo/:id', 'routes/todo.tsx')
] satisfies RouteConfig

In the code above, we’re using a dynamic segment in the first argument of the route function (todo:id). We end up with a dynamic route matcher that matches the URL /todo/:id and renders the todo.tsx route module. The :id part is an argument that we’ll be used in the loader function to render the TODO with that id.

Adding a Layout Route

It’s pretty common to have a layout component shared amongst several routes in our application. As you can see, both our route modules have a main element repeated (in real life scenarios we’d have way more than that). We can define a layout route in the app/root.tsx file:

app/root.tsx
import { Layout } from '@react-router/dev/routes'
export default [
layout("./components/layout.tsx", [
index('routes/home.tsx'),
route('todo/:id', 'routes/todo.tsx')
]),
]

An this is the content of components/layout.tsx:

components/layout.tsx
import { Outlet } from 'react-router'
export default function Layout() {
return (
<div>
<header>
<h1>TODO list app</h1>
</header>
<main>
<Outlet />
</main>
</div>
)
}

So the way this works is that the Layout component is rendered for all the nested routes, wrapping them, and the Outlet component is used to render the child routes.

IMPORTANT

Every route in routes.ts is nested inside the special root route, defined in the app/root.tsx module (Remember the Layout component we mentioned at the beginning?).

Nested routes

We can also define nested routes in our application. Let’s say we want to have a

Adding a 404 Route

This one is really simple, we just have to add a route matcher that matches any URL:

app/routes.ts
import { type RouteConfig, index, route } from '@react-router/dev/routes'
export default [
index('routes/home.tsx'),
route('todo/:id', 'routes/todo.tsx'),
route('*', 'routes/not-found.tsx')
] satisfies RouteConfig

And the content of routes/not-found.tsx:

routes/not-found.tsx
export default function NotFound() {
return <h1>404 - Not Found</h1>
}
Cool pic

Web Dev CLI Setup for macOS 💻 🍎

First things I do to start burning oil as a Web Dev

It’s not so often we get to set up a mac with the basic command line tools that make you productive, here I’ll leave what I do.

Command Line Tools

If you’re gonna be writing apps for iOS or macOS, most probably you should be installing Xcode, but if that’s not the case, probably it’s enough to install the command line tools:

Terminal window
xcode-select –-install

NOTE

Trying to run an unknown command such as git, will also cause the system to prompt us to install the command line tools.

To verify that the command line tools have been installed, you can run:

Terminal window
xcode-select -p

The output of the command above should be the location of the **command line tools in our system, in my case /Library/Developer/CommandLineTools. If you’re curious about what tools exactly are we getting, run:

Terminal window
ls Library/Developer/CommandLineTools/usr/bin

Homebrew: the macOS Package Manager

Homebrew is the most popular package manager for macOS, which we can install with:

Terminal window
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

Oh My Zsh

Oh My Zsh is an open source, community-driven framework for managing your Zsh configuration. Installing it is super easy:

Terminal window
sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"

If you restart your shell, you’ll see you have a pretty cool new prompt. Next, let’s install some plugins:

  • Autosuggestions plugin, which suggests commands as you type based on history and completions. Read installation instructions here
  • zsh-syntax-highlighting plugin, which enables highlighting of commands whilst they are typed at a zsh prompt into an interactive terminal. This is super helpful for catching typos that would result in syntax errors. Read how to install it in oh my zsh here
  • zsh-autocomplete plugin, which provides real-time type-ahead autocompletion to your command line. This one doesn’t include instructions about how to install in oh my zsh. Basically we just have to clone it in the plugins folder:
Terminal window
git clone --depth 1 -- https://github.com/marlonrichert/zsh-autocomplete.git $ZSH_CUSTOM/plugins/zsh-autocomplete

NOTE

Note how above we’re using the ZSH_CUSTOM environment variable, which is used in Oh My Zsh to specify a custom directory for your plugins, themes, and custom configurations.

Once we’ve installed the plugins we want, we have to add them to the list of plugins in a our .zshrc file; this is what my list looks like:

Terminal window
plugins=(
git
zsh-autosuggestions
zsh-syntax-highlighting
zsh-autocomplete
)

To uninstall any of the plugins, we just have to remove it from the list of plugins above, and remove its folder; for example, to remove the zsh-syntax-highlighting folder:

Terminal window
rm -rf $ZSH_CUSTOM/plugins/zsh-syntax-highlighting

FZF

Fzf is a command-line fuzzy finder which I find super useful. Let’s install it with brew:

Terminal window
brew install fzf

Here I had some problems integrating this tool with zsh, but searching through the internets I found out that we have to run an installation script to generate the necessary configuration files:

Terminal window
$(brew --prefix)/opt/fzf/install

NOTE

The $(brew --prefix) part is a command substitution that gives us the folder where Homebrew installs all the stuff; so if you run brew --prefix the output in my case, at the time of writing this, was /opt/homebrew (back in the day it was some other folder).

The output of the command above:

Terminal window
Downloading bin/fzf ...
- Already exists
- Checking fzf executable ... 0.61.3
Do you want to enable fuzzy auto-completion? ([y]/n)
Do you want to enable key bindings? ([y]/n)
Generate /Users/javi/.fzf.bash ... OK
Generate /Users/javi/.fzf.zsh ... OK
Do you want to update your shell configuration files? ([y]/n)
Update /Users/javi/.bashrc:
- [ -f ~/.fzf.bash ] && source ~/.fzf.bash
+ Added
Update /Users/javi/.zshrc:
- [ -f ~/.fzf.zsh ] && source ~/.fzf.zsh
+ Added
Finished. Restart your shell or reload config file.
source ~/.bashrc # bash (.bashrc should be loaded from .bash_profile)
source /Users/javi/.zshrc # zsh
Use uninstall script to remove fzf.

At the end, we should end up with a line at the end of our .zshrc:

Terminal window
[ -f ~/.fzf.zsh ] && source ~/.fzf.zsh

Which checks that the generated ~/.fzf.zsh file exists, and source it.

Amazon Q

A friend of mine recommended me a (generative AI)-powered assistant named Amazon Q, which is quite easy to install in macOS:

Terminal window
brew install amazon-q

Or just download the .dmg file, and clickety-click until we have it running. Whatever way we choose, we can verify the installation with:

Terminal window
q --version

NVM

nvm is next:

Terminal window
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.2/install.sh | bash

Running the command above appends the following lines to the bottom of our .zshrc:

Terminal window
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion

To verify the installation we have to run:

Terminal window
command -v nvm

which should output nvm if the installation was successful. Please note that which nvm will not work, since nvm is a sourced shell function, not an executable binary.

NOTE

To download, compile, and install the latest release of Node.js, do this:

Terminal window
nvm install node # "node" is an alias for the latest version